package edu.northwestern.cbits.purple_robot_manager;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.NullCipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.net.Uri;
import android.net.Uri.Builder;
import android.preference.PreferenceManager;
import android.util.Base64;
import edu.emory.mathcs.backport.java.util.Arrays;
import edu.northwestern.cbits.purple_robot_manager.activities.settings.SettingsKeys;
import edu.northwestern.cbits.purple_robot_manager.config.LegacyJSONConfigFile;
import edu.northwestern.cbits.purple_robot_manager.logging.LogManager;
public class EncryptionManager
{
private static final String CRYPTO_ALGORITHM = "AES/CBC/PKCS5Padding";
private HashMap<String, String> _cachedHashes = new HashMap<>();
private static final EncryptionManager _instance = new EncryptionManager();
private boolean _configurationReady = false;
private EncryptionManager()
{
if (EncryptionManager._instance != null)
throw new IllegalStateException("Already instantiated");
}
public static EncryptionManager getInstance()
{
return EncryptionManager._instance;
}
protected static SharedPreferences getPreferences(Context context)
{
return PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
}
public Cipher encryptCipher(Context context, boolean functional)
{
Cipher cipher = new NullCipher();
if (functional)
{
try
{
SecretKeySpec secretKey = this.keyForCipher(context, EncryptionManager.CRYPTO_ALGORITHM);
IvParameterSpec ivParameterSpec = new IvParameterSpec(this.getIVBytes());
cipher = Cipher.getInstance(EncryptionManager.CRYPTO_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);
}
catch (UnsupportedEncodingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException e)
{
throw new RuntimeException(e);
}
}
return cipher;
}
public Cipher decryptCipher(Context context, boolean functional)
{
Cipher cipher = new NullCipher();
if (functional)
{
try
{
SecretKeySpec secretKey = this.keyForCipher(context, EncryptionManager.CRYPTO_ALGORITHM);
IvParameterSpec ivParameterSpec = new IvParameterSpec(this.getIVBytes());
cipher = Cipher.getInstance(EncryptionManager.CRYPTO_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
}
catch (UnsupportedEncodingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException e)
{
throw new RuntimeException(e);
}
}
return cipher;
}
public String createHash(Context context, String string, String algorithm)
{
if (string == null)
return null;
String hash = null;
try
{
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] digest = md.digest(string.getBytes("UTF-8"));
hash = (new BigInteger(1, digest)).toString(16);
while (hash.length() < 32)
{
hash = "0" + hash;
}
}
catch (NoSuchAlgorithmException | UnsupportedEncodingException e)
{
LogManager.getInstance(context).logException(e);
}
return hash;
}
public String getUserId(Context context)
{
SharedPreferences prefs = EncryptionManager.getPreferences(context);
String userId = prefs.getString(SettingsKeys.USER_ID_KEY, null);
if (userId == null)
{
userId = "unknown-user";
AccountManager manager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
Account[] list = manager.getAccountsByType("com.google");
if (list.length == 0)
list = manager.getAccounts();
if (list.length > 0)
userId = list[0].name;
Editor e = prefs.edit();
e.putString(SettingsKeys.USER_ID_KEY, userId);
e.commit();
}
return userId;
}
public String getUserHash(Context context)
{
String userId = this.getUserId(context);
String hash = this._cachedHashes.get(userId);
if (hash == null)
{
hash = this.createHash(context, userId);
this._cachedHashes.put(userId, hash);
}
return hash;
}
public SecretKeySpec keyForCipher(Context context, String cipherName) throws UnsupportedEncodingException
{
String userHash = this.getUserHash(context);
String keyString = (new StringBuffer(userHash)).reverse().toString();
if (cipherName != null && cipherName.startsWith("AES"))
{
byte[] stringBytes = keyString.getBytes("UTF-8");
byte[] keyBytes = new byte[32];
Arrays.fill(keyBytes, (byte) 0x00);
for (int i = 0; i < keyBytes.length && i < stringBytes.length; i++)
{
keyBytes[i] = stringBytes[i];
}
SecretKeySpec key = new SecretKeySpec(keyBytes, cipherName);
return key;
}
return this.keyForCipher(context, EncryptionManager.CRYPTO_ALGORITHM);
}
protected byte[] getIVBytes()
{
byte[] bytes =
{ (byte) 0xff, 0x00, 0x11, (byte) 0xee, 0x22, (byte) 0xdd, 0x33, (byte) 0xcc, 0x44, (byte) 0xbb, 0x55,
(byte) 0xaa, 0x66, (byte) 0x99, 0x77, (byte) 0x88 };
return bytes;
}
public void writeToEncryptedStream(Context context, OutputStream out, byte[] bytes, boolean functional)
throws IOException
{
CipherOutputStream cout = new CipherOutputStream(out, this.encryptCipher(context, functional));
cout.write(bytes);
cout.flush();
cout.close();
}
public byte[] readFromEncryptedStream(Context context, InputStream in, boolean functional) throws IOException
{
CipherInputStream cin = new CipherInputStream(in, this.decryptCipher(context, functional));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int read = 0;
while ((read = cin.read(buffer, 0, buffer.length)) != -1)
{
baos.write(buffer, 0, read);
}
cin.close();
return baos.toByteArray();
}
public String fetchEncryptedString(Context context, String key)
{
key = this.createHash(context, key);
SharedPreferences prefs = EncryptionManager.getPreferences(context);
String encoded = prefs.getString(key, null);
if (encoded != null)
{
try
{
byte[] baseDecoded = Base64.decode(encoded, Base64.DEFAULT);
byte[] decoded = this.decryptCipher(context, true).doFinal(baseDecoded);
return new String(decoded, "UTF-8");
}
catch (IllegalBlockSizeException | UnsupportedEncodingException | BadPaddingException e)
{
LogManager.getInstance(context).logException(e);
}
}
return null;
}
public String encryptString(Context context, String value) throws IllegalBlockSizeException, BadPaddingException,
UnsupportedEncodingException
{
byte[] encoded = this.encryptCipher(context, true).doFinal(value.getBytes("UTF-8"));
String baseEncoded = Base64.encodeToString(encoded, Base64.DEFAULT);
return baseEncoded;
}
public boolean persistEncryptedString(Context context, String key, String value)
{
try
{
SharedPreferences prefs = EncryptionManager.getPreferences(context);
Editor edit = prefs.edit();
key = this.createHash(context, key);
if (value != null)
{
byte[] encoded = this.encryptCipher(context, true).doFinal(value.getBytes("UTF-8"));
String baseEncoded = Base64.encodeToString(encoded, Base64.DEFAULT);
edit.putString(key, baseEncoded);
}
else
edit.remove(key);
return edit.commit();
}
catch (IllegalBlockSizeException | UnsupportedEncodingException | BadPaddingException e)
{
LogManager.getInstance(context).logException(e);
}
return false;
}
public void setConfigUri(Context context, Uri configUri)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
Editor e = prefs.edit();
if (configUri != null)
{
Builder builder = new Builder();
builder.scheme(configUri.getScheme());
builder.encodedAuthority(configUri.getAuthority());
if (configUri.getPath() != null)
builder.encodedPath(configUri.getPath());
if (configUri.getFragment() != null)
builder.encodedFragment(configUri.getFragment());
String query = configUri.getQuery();
ArrayList<String> keys = new ArrayList<>();
if (query != null)
{
String[] params = query.split("&");
for (String param : params)
{
String[] components = param.split("=");
keys.add(components[0]);
}
}
for (String key : keys)
{
if ("user_id".equals(key))
{
// Skip for now.
}
else
builder.appendQueryParameter(key, configUri.getQueryParameter(key));
}
builder.appendQueryParameter("user_id", this.getUserId(context));
configUri = builder.build();
if (!prefs.getString(SettingsKeys.CONFIG_URL, "---").equals(configUri.toString()))
{
e.remove(LegacyJSONConfigFile.JSON_LAST_UPDATE);
e.remove(LegacyJSONConfigFile.JSON_LAST_HASH);
this.setConfigurationReady(false);
}
e.putString(SettingsKeys.CONFIG_URL, configUri.toString());
}
else
e.remove(SettingsKeys.CONFIG_URL);
e.commit();
}
public Uri getConfigUri(Context context)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String uriString = prefs.getString(SettingsKeys.CONFIG_URL, null);
if (uriString != null)
{
Uri uri = Uri.parse(uriString);
Builder builder = new Builder();
builder.scheme(uri.getScheme());
builder.encodedAuthority(uri.getAuthority());
if (uri.getPath() != null)
builder.encodedPath(uri.getPath());
if (uri.getFragment() != null)
builder.encodedFragment(uri.getFragment());
String query = uri.getQuery();
ArrayList<String> keys = new ArrayList<>();
if (query != null)
{
String[] params = query.split("&");
for (String param : params)
{
String[] components = param.split("=");
keys.add(components[0]);
}
}
for (String key : keys)
{
if ("user_id".equals(key)) {
try {
this.setUserId(context, uri.getQueryParameter(key));
}
catch (UnsupportedOperationException e)
{
LogManager.getInstance(context).logException(e);
}
}
else
builder.appendQueryParameter(key, uri.getQueryParameter(key));
}
builder.appendQueryParameter("user_id", this.getUserId(context));
uri = builder.build();
return uri;
}
return null;
}
public void restoreDefaultId(Context context)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
Editor e = prefs.edit();
e.remove(SettingsKeys.USER_ID_KEY);
e.commit();
LogManager.getInstance(context).log("restored_default_user_id", null);
}
public void setUserId(Context context, String userId)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (userId != null && userId.trim().length() == 0)
{
Editor e = prefs.edit();
e.remove(SettingsKeys.USER_ID_KEY);
e.commit();
userId = this.getUserId(context);
}
HashMap<String, Object> payload = new HashMap<>();
payload.put("source", "EncryptionManager");
payload.put("new_id", userId);
payload.put("old_id", prefs.getString(SettingsKeys.USER_ID_KEY, ""));
LogManager.getInstance(context).log("set_user_id", payload);
Editor e = prefs.edit();
e.putString(SettingsKeys.USER_ID_KEY, userId);
e.commit();
this.setConfigUri(context, Uri.parse(prefs.getString(SettingsKeys.CONFIG_URL, context.getString(R.string.json_config_url))));
}
public String createHash(Context context, String name)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String algorithm = prefs.getString("config_preferred_hash_function", "MD5");
try
{
MessageDigest.getInstance(algorithm);
}
catch (NoSuchAlgorithmException e)
{
// Fall back to MD5 if SHA-2 is not present...
algorithm = "MD5";
}
return this.createHash(context, name, algorithm);
}
public void setConfigurationReady(boolean isReady)
{
this._configurationReady = isReady;
}
public boolean getConfigurationReady()
{
return this._configurationReady;
}
public static String normalizedPhoneHash(Context context, String phoneNumber)
{
if (phoneNumber == null)
return null;
String hash = null;
try
{
MessageDigest md = MessageDigest.getInstance("MD5");
phoneNumber = phoneNumber.replaceAll("[^\\d.]", "");
while (phoneNumber.length() > 10)
phoneNumber = phoneNumber.substring(1);
while (phoneNumber.length() < 10)
phoneNumber += "0";
byte[] digest = md.digest(phoneNumber.getBytes("UTF-8"));
hash = (new BigInteger(1, digest)).toString(16);
while (hash.length() < 32)
{
hash = "0" + hash;
}
}
catch (NoSuchAlgorithmException e)
{
LogManager.getInstance(context).logException(e);
}
catch (UnsupportedEncodingException e)
{
LogManager.getInstance(context).logException(e);
}
return hash;
}
}